Skip to content

[TMP] Feature Preview - #510

Draft
nicklafleur wants to merge 9 commits into
boxed:mainfrom
lyft:nicklafleur/oss-complete
Draft

[TMP] Feature Preview#510
nicklafleur wants to merge 9 commits into
boxed:mainfrom
lyft:nicklafleur/oss-complete

Conversation

@nicklafleur

Copy link
Copy Markdown
Collaborator

This PR serves as a public preview of our in-flight work

nicklafleur and others added 9 commits August 14, 2026 12:26
Config still exposed Config.get()/ensure_loaded()/reset() classmethods while
MutmutState (added by boxed#509) already used a bare config()/state()-style
accessor. Replace the classmethod API with module-level config()/reset_config()
functions to match, and migrate the remaining module-level globals in
mutmut/__init__.py (stats_time, duration_by_test, tests_by_mangled_function_name,
_stats, _covered_lines) onto MutmutState, keeping deprecated __getattr__ shims
for external readers of the old mutmut.* attributes.

Renames local variables that shadowed the new config()/state() function names
where needed to avoid UnboundLocalError, and repoints test fixtures/monkeypatches
that patched Config.get() at the class/module level to instead mutate the live
config()/state() singleton instances in place, so patches stay visible to
modules that already imported config by reference.
Move timeout management from threading/ to workers/ package.

- Move src/mutmut/threading/timeout.py -> src/mutmut/workers/timeout.py
- Update __main__.py import to use new location
- Move tests/threading/ -> tests/workers/
Non-functional reorganization: move cohesive groups of code out of the
oversized __main__.py into dedicated modules. No behavior change.

New/extended modules:
- stats.py — status/emoji maps, Stat, collect_stat,
  calculate_summary_stats, print_stats, load_stats, save_stats
- runners/harness.py — TestRunner ABC, PytestRunner, HammettRunner,
  ListAllTestsResult, collected_test_names, unused, and the
  test-runner exceptions (Collect/BadTestExecutionCommands)
- ui/browse.py — ResultBrowser Textual app wrapped in
  run_result_browser(); get_diff_for_mutant/apply_mutant are injected
  to avoid a circular import back into __main__. Uses upstream boxed#543's
  @work/Lock/get_current_worker diff-loading model.
- ui/terminal.py — spinner + status_printer/print_status
- utils/file_utils.py — walk_all_files/walk_source_files/
  walk_mutatable_files, copy_src_dir, copy_also_copy_files,
  setup_source_paths (alongside the existing change_cwd)

Move result_browser_layout.tcss into ui/ so Textual's CSS_PATH
resolves relative to ui/browse.py.

__main__.py re-imports the moved public names, so external
`from mutmut.__main__ import ...` call sites keep working unchanged.
Repoint code_coverage.py's TYPE_CHECKING import to runners.harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add pipe-based fork isolation helpers (run_in_fork_with_result, run_in_fork)
that run functions in forked children so the parent process never imports
pytest/conftest and stays fork-safe. This is the foundation for the upcoming
hot-fork runner where the parent acts purely as an orchestrator.

Also add OrchestratorCrashError, which reports the crashed orchestrator's exit
code, the lost in-flight mutants (truncated past 10), an optional crash-log
path, and instructions to resume with 'mutmut run'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a MutantRunner ABC that owns the process-isolation strategy for
testing mutants and also fronts the surrounding test operations (stats
collection, clean tests, forced-fail, test listing), so __main__ no longer
drives os.fork() directly.

ForkRunner encapsulates the traditional os.fork()-per-mutant loop that lived
inline in _run(): submit() forks a child under a CPU/wall timeout, and
wait_for_result() reaps one child into a MutantResult. get_mutant_runner()
selects the runner from the new process_isolation config (ProcessIsolation
enum; only 'fork' is wired up here, 'hot-fork' raises pending a later commit).

_run() now drives the runner through submit/has_capacity/wait_for_result/
pending_count/shutdown and registers results by mutant name. The stale-stats
protections are preserved verbatim: _check_test_to_mutant_associations() still
runs, and collect_or_load_stats() keeps its apply_config_invalidation path
(now routed through MutantRunner.collect_stats/list_all_tests). Behavior for
the default fork path is unchanged; the full suite (incl. the e2e run) is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a single-orchestrator process-isolation strategy for projects whose test
setup makes the parent fork-unsafe (gevent monkey-patching, grpc, torch):

    parent (clean) -> orchestrator (imports pytest once) -> N grandchildren

The parent never imports pytest/conftest. The orchestrator imports pytest a
single time, warms up (configurable via hot_fork_warmup: collect/import/none),
then forks one grandchild per mutant, streaming results back over a pipe and
reaping via a SIGCHLD self-pipe. If the orchestrator crashes, in-flight mutants
are re-submitted to a fresh orchestrator up to max_orchestrator_restarts times
before raising OrchestratorCrashError. Stats/clean-test/forced-fail/test-listing
all run in short-lived forks (StatsResult carries the collected mapping back to
the parent), so the parent stays clean throughout.

Selected via process_isolation = "hot-fork"; get_mutant_runner() now builds it.
Supporting pieces: HotForkWarmup config + validation, TestRunner.warm_up(),
models/results.StatsResult, and utils/logging_utils for the orchestrator's
file-only logging and crash logs.

Validated end-to-end: on the my_lib project, hot-fork produces byte-identical
verdicts to fork (112 mutants, same 37/64/10/1 distribution). Adds a subprocess
e2e smoke test (hot_fork_basic) plus factory/config-validation unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Groundwork for the result browser's dependency visualization and cache
tracking. Pure logic, no Textual imports, so it is unit-tested on its own:

- models/cache_status.CacheStatus (CACHED/STALE_DEPENDENCY/INVALID) with
  severity ordering + CACHE_STATUS_EMOJI.
- ui/helpers: dependency BFS over state().function_dependencies
  (expand_changed_functions, get_ordered_upstream_and_downstream_functions,
  compute_funcs_with_invalid_deps, find_invalid_dependencies) and per-mutant
  get_cache_status.
- format_utils.mangled_name_from_mutant_name + raw_func_name_from_mangled
  (mangled<->raw name conversion for dependency lookup).
- Config.get_effective_dependency_depth() (tracking depth clamped to
  max_stack_depth), adapted to this branch's -1 sentinel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Layer the dependency/cache features onto the boxed#543 browser (keeping its
@work/Lock/get_current_worker diff loader untouched):

- A two-column upstream(callers)/downstream(callees) dependencies table shown
  beside the diff (3:2), populated by BFS over state().function_dependencies.
- A cache-status column on both the files and mutants tables (CACHED/STALE/
  INVALID) computed from per-function hash changes + invalid-dependency
  propagation; the files table rolls up to its worst mutant.
- A depth toggle (v) cycling 1-lvl / configured-depth / full, with per-(func,
  depth) BFS memoization.
- Retest-invalid-dependencies (d) and, to make the browser's 'g' binding real,
  a new `generate` CLI command that regenerates mutants and refreshes hashes/
  stats without running the mutation loop (`--no-invalidate-callers` keeps the
  caller edges). The generation prefix of `_run` is extracted into a shared
  `_generate_mutants_and_collect_stats` helper so both commands stay in sync.

Kept this branch's string-based status handling (status_by_exit_code/
emoji_by_status) rather than pulling in the reference's MutantStatus enum, so
the diff stays confined to the new features.

Validated: headless Pilot mount test (files/mutants/deps tables populate, cache
column present, depth toggle works) + a subprocess `generate` e2e. Full suite
399 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidate the mangled-name helpers into `utils/format_utils.py` so they live
next to the other key/name utilities and can be imported without pulling in
`__main__` or `trampoline_templates`:

- Move `CLASS_NAME_SEPARATOR` + `mangle_function_name` out of
  `mutation/trampoline_templates.py` (which no longer referenced them beyond the
  definition) into `format_utils`; `file_mutation` now imports
  `mangle_function_name` from there.
- Move `mangled_name_from_mutant_name` + `orig_function_and_class_names_from_key`
  out of `__main__` into `format_utils`, removing the temporary
  `mangled_name_from_mutant_name` duplicate introduced with the browser helpers.
  `__main__` re-imports both (so `from mutmut.__main__ import ...` still works),
  and `mutation/trampoline.py` now imports the helper from `format_utils`,
  dropping one `__main__` import cycle.

`format_utils` is now a leaf module (stdlib only). Pure move, no behavior change;
full suite is green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nicklafleur
nicklafleur force-pushed the nicklafleur/oss-complete branch from ca63580 to 06ce4cf Compare August 14, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant